﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;

namespace PlusPlusPaint.Layers
{
    class LayerManager
    {
        #region member variables
        // Chosen to not be an interface to allow access to AddRange.
        List<ILayer> layers = new List<ILayer>();
        #endregion

        #region properties
        public int LayerCount
        {
            get
            {
                return layers.Count;
            }
        }
        #endregion

        #region events
        public event LayerManagerHandler LayerAdded;
        public event LayerManagerHandler LayerRemoved;
        public event LayerManagerHandler LayerOrderChanged;
        public event LayerManagerHandler LayerVisibilityChanged;
        public event EventHandler Invalidated;
        #endregion

        #region public methods
        public Image GetVisualRepresentationWithDimensions(int x, int y)
        {
            Image surface = new Bitmap(x, y);
            var visibleLayers = from layer in layers // We don't care about the type, really.
                                where layer.Visible
                                select layer;
            foreach (ILayer l in visibleLayers)
                l.BurnOn(surface);
            return surface;
        }

        public Image GetVisualRepresentationWithDimensions(Size s)
        {
            return GetVisualRepresentationWithDimensions(s.Width, s.Height);
        }

        public void AddVisualObjectLayerAt(int index)
        {
            VisualObjectLayer layer = new VisualObjectLayer();
            layers.Insert(index, layer);
            this.raiseLayerAdded(new LayerManagerEventArgs(index, index, null, new LayerSummary(layer)));
            this.Invalidate();
        }

        public void AddLayerAt(ILayer layer, int index)
        {
            Debug.Assert(!layers.Contains(layer), "Layer added twice.");
            layers.Add(layer);
            layer.CallOnDeletion(() => this.RemoveLayer(new LayerSummary(layer)));
            layer.CallOnInvalidate((object o, EventArgs ea) => this.Invalidate());
            this.raiseLayerAdded(new LayerManagerEventArgs(index, index, null, new LayerSummary(layer)));
            this.Invalidate();
        }

        public void AddLayer(ILayer layer)
        {
            AddLayerAt(layer, layers.Count);
        }

        public void AddLayerRange(IList<ILayer> newLayers)
        {
            foreach (ILayer l in newLayers)
                this.AddLayer(l);
        }

        public void RemoveLayer(LayerSummary ls)
        {
            int index = GetIndexOf(ls);
            Debug.Assert(index != -1, "Invalid layer specified;  should never happen.");
            RemoveLayer(index);
        }

        public void RemoveLayer(int index)
        {
            if (index >= layers.Count) // Silently ignore weirdos making unreasonable requests.
                return;
            ILayer layer = layers[index];
            layers.Remove(layer);
            this.LayerRemoved(this, new LayerManagerEventArgs(index, index, new LayerSummary(layer), null));
            this.Invalidate();
        }

        /// <summary>
        /// Remove all layers
        /// </summary>
        public void Drop()
        {
            for (int layerCount = LayerCount - 1; layerCount >= 0; layerCount--)
                this.raiseLayerRemoved(new LayerManagerEventArgs(layerCount, layerCount, null, new LayerSummary(layers[layerCount])));
            layers.Clear();
            this.Invalidate();
        }

        public void ChangeLayerVisiblity(LayerSummary ls, bool visible)
        {
            this.ChangeLayerVisiblity(GetIndexOf(ls), visible);
        }

        public void ChangeLayerVisiblity(int index, bool visible)
        {
            layers[index].Visible = visible;
            this.raiseLayerVisibilityChanged(new LayerManagerEventArgs(index, index, null, null));
            this.Invalidate();
        }

        public void Invalidate()
        {
            this.Invalidated(this, new EventArgs());
        }

        public void ApplyToolAtPoint(Tools.ISketchTool t, Point p)
        {
            if (t.DemandsNewLayer) // If the tool really wants a new layer, let's give it that.
            {
                this.AddLayer(t.SuggestNewLayer());
                layers.Last().ApplyToolToPoint(t, p);
                return;
            }
            ILayer applicableLayer = layers.LastOrDefault((layer) => // Look for a layer that we can use
                {
                    return layer.Visible && layer.CanHaveToolApplied(t) && layer.PositionIsInHitbox(p);
                });
            if (applicableLayer != null)
            {
                applicableLayer.ApplyToolToPoint(t, p);
                return;
            }
            if (t.AcceptsNewLayer)
            {
                this.AddLayer(t.SuggestNewLayer());
                layers.Last().ApplyToolToPoint(t, p);
            }
            // If a tool can't affect anything currently existing, and doesn't
            // want anything new, just leave it be.
        }

        /// <summary>
        /// Return a summary of the layer at the given point.  If there is no
        /// layer found, return null.  (NOT an invalid LayerSummary!)
        /// </summary>
        public LayerSummary SelectLayerAt(Point p)
        {
            ILayer l = layers.Last((layer) => layer.PositionIsInHitbox(p));
            return l != null ? new LayerSummary(l) : null;
        }

        /// <summary>
        /// Return a summary of the layer at position n.
        /// </summary>
        public LayerSummary SelectLayerNumberN(int n)
        {
            return new LayerSummary(layers[n]);
        }

        /// <summary>
        /// Return the index of the layer.  Should only ever be called
        /// with LayerSummaries that are known to currently be in the list.
        /// </summary>
        public int GetIndexOf(LayerSummary ls)
        {
            return layers.FindIndex((layer) => ls.IsLayer(layer));
        }

        public void MoveUpN(int layerIndex, int n)
        {
            if (n == 0)
                return;
            if (n < 0)
            {
                MoveDownN(layerIndex, -n); // Buggers calling the wrong function.
                return;
            }
            int currentpos = layerIndex;
            int targetpos;
            if (currentpos + n >= layers.Count) // If it must be moved further than the end, just move to the end
                targetpos = layers.Count - 1;
            else
                targetpos = currentpos + n;
            ILayer layertemp = layers[currentpos];
            layers.RemoveAt(currentpos);
            layers.Insert(targetpos, layertemp);
            this.raiseLayerOrderChanged(new LayerManagerEventArgs(currentpos, targetpos, null, null));
            this.Invalidate();
        }

        public void MoveDownN(int layerIndex, int n)
        {
            if (n == 0)
                return;
            if (n < 0)
            {
                MoveUpN(layerIndex, -n);
                return;
            }
            int currentpos = layerIndex;
            int targetpos;
            if (currentpos - n < 0) // Can't go below 0
                targetpos = 0;
            else
                targetpos = currentpos - n;
            ILayer layertemp = layers[currentpos];
            layers.RemoveAt(currentpos);
            layers.Insert(targetpos, layertemp);
            this.raiseLayerOrderChanged(new LayerManagerEventArgs(currentpos, targetpos, null, null));
            this.Invalidate();
        }

        public void Duplicate(LayerSummary ls)
        {
            layers.Add(GetLayerBySummary(ls).Clone());
        }

        public void Explode(LayerSummary ls)
        {
            int index = GetIndexOf(ls);
            if (!layers[index].Explodable)
                return;
            IList<ILayer> layersToAdd = layers[index].ExplodeLayer();
            RemoveLayer(ls);
            for (int i = 0; i < layersToAdd.Count; ++i)
                AddLayerAt(layersToAdd[i], index+i);
            this.Invalidate();
        }

        public void Group(IList<LayerSummary> layersToGroup)
        {
            List<int> indices = (from ls in layersToGroup
                                 select GetIndexOf(ls)).ToList<int>();
            indices.Sort();
            CompositeLayer cl = new CompositeLayer();
            foreach (int i in indices)
                cl.Add(layers[i]);
            indices.Reverse();
            foreach (int i in indices)
                this.RemoveLayer(i);
            this.AddLayer(cl);
            this.Invalidate();
        }

        public SerializedObject Serialize()
        {
            List<SerializedObject> subobjects = new List<SerializedObject>();
            foreach (ILayer layer in layers)
                subobjects.Add(layer.Serialize());
            return new SerializedObject("LayerManager", unnamedSubobjects: subobjects);
        }

        public void Deserialize(SerializedObject toplevel)
        {
            Drop();
            foreach (SerializedObject so in toplevel.UnnamedSubobjects)
            {
                ILayer l = MakeLayerByName(so.Name);
                l.Deserialize(so);
                AddLayer(l);
            }
        }
        #endregion

        #region static public methods
        static public ILayer MakeLayerByName(string name)
        {
            switch (name.ToLower())
            {
                case "compositelayer":
                    return new CompositeLayer();
                case "visualobjectlayer":
                    return new VisualObjectLayer();
                default:
                    throw new Exception("Unknown layer type: " + name);
            }
        }
        #endregion

        #region private methods
        private ILayer GetLayerBySummary(LayerSummary ls)
        {
            return layers.First((layer) => ls.IsLayer(layer));
        }

        private void raiseLayerVisibilityChanged(LayerManagerEventArgs lmea)
        {
            if (LayerVisibilityChanged != null)
                LayerVisibilityChanged(this, lmea);
        }

        private void raiseLayerOrderChanged(LayerManagerEventArgs lmea)
        {
            if (LayerOrderChanged != null)
                LayerOrderChanged(this, lmea);
        }

        private void raiseLayerRemoved(LayerManagerEventArgs lmea)
        {
            if (LayerRemoved != null)
                LayerRemoved(this, lmea);
        }

        private void raiseLayerAdded(LayerManagerEventArgs lmea)
        {
            if (LayerAdded != null)
                LayerAdded(this, lmea);
        }
        #endregion
    }
}
